package com.dbflow5.processor.definition.column;

import com.dbflow5.StringUtils;
import com.dbflow5.processor.ClassNames;
import com.dbflow5.processor.SQLiteHelper;
import com.dbflow5.processor.definition.behavior.Behaviors;
import com.dbflow5.processor.utils.ModelUtils;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.NameAllocator;
import com.squareup.javapoet.TypeName;

public abstract class ColumnAccessCombiner {

    private final NameAllocator nameAllocator = new NameAllocator();

    public Combiner combiner;

    public ColumnAccessCombiner(Combiner combiner) {
        this.combiner = combiner;
    }

    public CodeBlock getFieldAccessBlock(CodeBlock.Builder existingBuilder,
                                         CodeBlock modelBlock,
                                         boolean useWrapper,
                                         boolean defineProperty) {
        CodeBlock fieldAccess;
        if (combiner.wrapperLevelAccessor != null && !combiner.fieldTypeName.isPrimitive()) {
            fieldAccess = CodeBlock.of(nameAllocator.newName(combiner.customPrefixName) + "ref" + combiner.fieldLevelAccessor.propertyName);

            if (defineProperty) {
                CodeBlock fieldAccessorBlock = combiner.fieldLevelAccessor.get(modelBlock);
                CodeBlock wrapperAccessorBlock = combiner.wrapperLevelAccessor.get(fieldAccessorBlock);
                // if same, don't extra null check.
                if (!combiner.fieldLevelAccessor.toString().equals(combiner.wrapperLevelAccessor.toString())
                        && !(combiner.wrapperLevelAccessor instanceof ColumnAccessor.TypeConverterScopeColumnAccessor)) {
                    existingBuilder.addStatement("$T $L = $L != null ? $L : null",
                            combiner.wrapperFieldTypeName, fieldAccess, fieldAccessorBlock, wrapperAccessorBlock);
                } else if (combiner.wrapperLevelAccessor instanceof ColumnAccessor.TypeConverterScopeColumnAccessor) {
                    existingBuilder.addStatement("$T $L = $L", combiner.wrapperFieldTypeName,
                            fieldAccess, wrapperAccessorBlock);
                } else {
                    existingBuilder.addStatement("$T $L = $L", combiner.wrapperFieldTypeName,
                            fieldAccess, fieldAccessorBlock);
                }
            }
        } else {
            if (useWrapper && combiner.wrapperLevelAccessor != null) {
                fieldAccess = combiner.wrapperLevelAccessor.get(combiner.fieldLevelAccessor.get(modelBlock));
            } else {
                fieldAccess = combiner.fieldLevelAccessor.get(modelBlock);
            }
        }
        return fieldAccess;
    }

    public abstract void addCode(CodeBlock.Builder builder, String columnRepresentation, CodeBlock defaultValue,
                                           int index,
                                           CodeBlock modelBlock,
                                           boolean defineProperty);

    public void addNull(CodeBlock.Builder code, String columnRepresentation, int index) {

    }

    public static class Combiner {
        public ColumnAccessor fieldLevelAccessor;
        public TypeName fieldTypeName;
        public ColumnAccessor wrapperLevelAccessor;
        public TypeName wrapperFieldTypeName;
        public ColumnAccessor subWrapperAccessor;
        public String customPrefixName;

        public Combiner(ColumnAccessor fieldLevelAccessor, TypeName fieldTypeName, ColumnAccessor wrapperLevelAccessor,
                        TypeName wrapperFieldTypeName, ColumnAccessor subWrapperAccessor, String customPrefixName) {
            this.fieldLevelAccessor = fieldLevelAccessor;
            this.fieldTypeName = fieldTypeName;
            this.wrapperLevelAccessor = wrapperLevelAccessor;
            this.wrapperFieldTypeName = wrapperFieldTypeName;
            this.subWrapperAccessor = subWrapperAccessor;
            this.customPrefixName = customPrefixName;
        }
    }


    public static class SimpleAccessCombiner extends ColumnAccessCombiner {
        public SimpleAccessCombiner(Combiner combiner) {
            super(combiner);
        }

        public void addCode(CodeBlock.Builder builder, String columnRepresentation, CodeBlock defaultValue,
                            int index, CodeBlock modelBlock, boolean defineProperty) {
            builder.addStatement("return $L", getFieldAccessBlock(builder, modelBlock,true,true));
        }

    }

    public static class ExistenceAccessCombiner extends ColumnAccessCombiner {
        public boolean autoRowId;
        public boolean quickCheckPrimaryKey;
        public ClassName tableClassName;

        public ExistenceAccessCombiner(Combiner combiner,
                                       boolean autoRowId,
                                       boolean quickCheckPrimaryKey,
                                       ClassName tableClassName) {
            super(combiner);
            this.autoRowId = autoRowId;
            this.quickCheckPrimaryKey = quickCheckPrimaryKey;
            this.tableClassName = tableClassName;
        }

        public void addCode(CodeBlock.Builder builder, String columnRepresentation, CodeBlock defaultValue,
                            int index, CodeBlock modelBlock, boolean defineProperty) {

            if (autoRowId) {
                CodeBlock access = getFieldAccessBlock(builder, modelBlock, true, true);

                builder.add("return ");

                if (!combiner.fieldTypeName.isPrimitive()) {
                    builder.add("($L != null && ", access);
                }
                builder.add("$L > 0", access);

                if (!combiner.fieldTypeName.isPrimitive()) {
                    builder.add(" || $L == null)", access);
                }
            }

            if (!autoRowId || !quickCheckPrimaryKey) {
                if (autoRowId) {
                    builder.add("\n&& ");
                } else {
                    builder.add("return ");
                }

                builder.add("$T.selectCountOf()\n.from($T.class)\n" +
                                ".where(getPrimaryConditionClause($L))\n" +
                                ".hasData(wrapper)",
                        ClassNames.SQLITE, tableClassName, modelBlock);
            }
            builder.add(";\n");
        }

    }

    public static class ContentValuesCombiner extends ColumnAccessCombiner {

        public ContentValuesCombiner(Combiner combiner) {
            super(combiner);
        }

        @Override
        public void addCode(CodeBlock.Builder builder, String columnRepresentation, CodeBlock defaultValue,
                            int index, CodeBlock modelBlock, boolean defineProperty) {
            CodeBlock fieldAccess = getFieldAccessBlock(builder, modelBlock,true,true);
            if (combiner.fieldTypeName.isPrimitive()) {
                builder.addStatement("values.put($1S, $2L)", StringUtils.quote(columnRepresentation), fieldAccess);
            } else {
                if (defaultValue != null) {
                    CodeBlock subWrapperFieldAccess = fieldAccess;
                    if (combiner.subWrapperAccessor != null) {
                        subWrapperFieldAccess = combiner.subWrapperAccessor.get(fieldAccess);
                    }
                    if (!fieldAccess.toString().equals(subWrapperFieldAccess.toString())
                            || !defaultValue.toString().equals("null")) {
                        builder.addStatement("values.put($S, $L != null ? $L : $L)",
                                StringUtils.quote(columnRepresentation), fieldAccess, subWrapperFieldAccess, defaultValue);
                    } else {
                        // if same default value is null and object reference is same as subwrapper.
                        builder.addStatement("values.put($S, $L)",
                                StringUtils.quote(columnRepresentation), fieldAccess);
                    }
                } else {
                    builder.addStatement("values.put($S, $L)",
                            StringUtils.quote(columnRepresentation), fieldAccess);
                }
            }
        }

        @Override
        public void addNull(CodeBlock.Builder code, String columnRepresentation, int index) {
            code.addStatement("values.putNull($S)", StringUtils.quote(columnRepresentation));
        }
    }

    public static class SqliteStatementAccessCombiner extends ColumnAccessCombiner {
        public SqliteStatementAccessCombiner(Combiner combiner) {
            super(combiner);
        }

        public void addCode(CodeBlock.Builder builder, String columnRepresentation,
                            CodeBlock defaultValue, int index,
                            CodeBlock modelBlock, boolean defineProperty) {
            CodeBlock fieldAccess = getFieldAccessBlock(builder, modelBlock,true, defineProperty);
            String wrapperMethod = SQLiteHelper.getWrapperMethod(combiner.wrapperFieldTypeName != null? combiner.wrapperFieldTypeName : combiner.fieldTypeName);
            String statementMethod = SQLiteHelper.get(combiner.wrapperFieldTypeName != null? combiner.wrapperFieldTypeName : combiner.fieldTypeName).sqLiteStatementMethod;

            String offset = index + " + " + columnRepresentation;
            if (StringUtils.isNullOrEmpty(columnRepresentation)) {
                offset = String.valueOf(index);
            }
            if (combiner.fieldTypeName.isPrimitive()) {
                builder.addStatement("statement.bind"+statementMethod+"("+offset+", "+fieldAccess+")");
            } else {
                if(combiner.subWrapperAccessor != null) {
                    CodeBlock subWrapperFieldAccess = combiner.subWrapperAccessor.get(fieldAccess) != null? combiner.subWrapperAccessor.get(fieldAccess) : fieldAccess;
                    if (!StringUtils.isNullOrEmpty(defaultValue.toString())) {
                        if(fieldAccess != null) {
                            builder.addStatement("statement.bind"+wrapperMethod+"("+offset+", "+subWrapperFieldAccess+")");
                        }else {
                            builder.addStatement("statement.bind"+statementMethod+"("+offset+", "+defaultValue+")");
                        }
                    } else {
                        if (combiner.subWrapperAccessor != null) {
                            builder.addStatement("statement.bind"+wrapperMethod+"OrNull("+offset+", "+fieldAccess+" != null ? "+subWrapperFieldAccess+" : null)");
                        } else {
                            builder.addStatement("statement.bind"+wrapperMethod+"OrNull("+offset+", "+subWrapperFieldAccess+")");
                        }

                    }
                }
            }
        }

        @Override
        public void addNull(CodeBlock.Builder code, String columnRepresentation, int index) {
            String access = index + " + " + columnRepresentation;
            if (columnRepresentation.isEmpty()) {
                access = String.valueOf(index);
            }
            code.addStatement("statement.bindNull("+access+")");
        }
    }

    public static class LoadFromCursorAccessCombiner extends ColumnAccessCombiner {

        public boolean hasDefaultValue;
        public NameAllocator nameAllocator;
        public Behaviors.CursorHandlingBehavior cursorHandlingBehavior;

        public LoadFromCursorAccessCombiner(Combiner combiner, boolean hasDefaultValue, NameAllocator nameAllocator, Behaviors.CursorHandlingBehavior cursorHandlingBehavior) {
            super(combiner);
            this.hasDefaultValue = hasDefaultValue;
            this.nameAllocator = nameAllocator;
            this.cursorHandlingBehavior = cursorHandlingBehavior;
        }

        @Override
        public void addCode(CodeBlock.Builder builder, String columnRepresentation, CodeBlock defaultValue,
                             int index, CodeBlock modelBlock, boolean defineProperty) {
            CodeBlock indexName;
            if (!cursorHandlingBehavior.orderedCursorLookup) {
                indexName = CodeBlock.of("\"" + columnRepresentation + "\"");
            } else {
                indexName = CodeBlock.of(String.valueOf(index));
            }

            if (combiner.wrapperLevelAccessor != null) {
                if (!cursorHandlingBehavior.orderedCursorLookup) {
                    indexName = CodeBlock.of(nameAllocator.newName("index_" + columnRepresentation, columnRepresentation));
                    builder.addStatement("$T $L = cursor.getColumnIndex($S)", Integer.class, indexName, columnRepresentation);
                    builder.beginControlFlow("if ($1L != -1 && !cursor.isNull($1L))", indexName);
                } else {
                    builder.beginControlFlow("if (!cursor.isNull($1L))", index);
                }
                CodeBlock cursorAccess = CodeBlock.of("cursor.$L($L)",
                        SQLiteHelper.getMethod(combiner.wrapperFieldTypeName !=null? combiner.wrapperFieldTypeName : combiner.fieldTypeName), indexName);
                // special case where we need to append try catch hack
                boolean isEnum = combiner.wrapperLevelAccessor instanceof ColumnAccessor.EnumColumnAccessor;
                if (isEnum) {
                    builder.beginControlFlow("try");
                }
                if (combiner.subWrapperAccessor != null) {
                    builder.addStatement(combiner.fieldLevelAccessor.set(
                            combiner.wrapperLevelAccessor.set(combiner.subWrapperAccessor.set(cursorAccess, null, false), null, false), modelBlock, false));
                } else {
                    builder.addStatement(combiner.fieldLevelAccessor.set(
                            combiner.wrapperLevelAccessor.set(cursorAccess, null, false), modelBlock, false));
                }
                if (isEnum) {
                    builder.nextControlFlow("catch ($T e)", IllegalArgumentException.class);
                    if (cursorHandlingBehavior.assignDefaultValuesFromCursor) {
                        builder.addStatement(combiner.fieldLevelAccessor.set(combiner.wrapperLevelAccessor.set(defaultValue, null,true), modelBlock, false));
                    } else {
                        builder.addStatement(combiner.fieldLevelAccessor.set(defaultValue, modelBlock, false));
                    }
                    builder.endControlFlow();
                }
                if (cursorHandlingBehavior.assignDefaultValuesFromCursor) {
                    builder.nextControlFlow("else");
                    builder.addStatement(combiner.fieldLevelAccessor.set(combiner.wrapperLevelAccessor.set(defaultValue, null,true), modelBlock, false));
                }
                builder.endControlFlow();
            } else {
                boolean hasDefault = hasDefaultValue;
                CodeBlock defaultValueBlock = defaultValue;
                if (!cursorHandlingBehavior.assignDefaultValuesFromCursor) {
                    defaultValueBlock = combiner.fieldLevelAccessor.get(modelBlock);
                } else if (!hasDefault && combiner.fieldTypeName.isBoxedPrimitive()) {
                    hasDefault = true; // force a null on it.
                }
                CodeBlock cursorAccess = CodeBlock.of("cursor.$LOrDefault($L"+(hasDefault? ", " + defaultValueBlock : "")+")",
                        SQLiteHelper.getMethod(combiner.wrapperFieldTypeName != null? combiner.wrapperFieldTypeName : combiner.fieldTypeName), indexName);
                builder.addStatement(combiner.fieldLevelAccessor.set(cursorAccess, modelBlock, false));
            }
        }
    }

    public static class PrimaryReferenceAccessCombiner extends ColumnAccessCombiner {
        public PrimaryReferenceAccessCombiner(Combiner combiner) {
            super(combiner);
        }

        public void addCode(CodeBlock.Builder builder, String columnRepresentation, CodeBlock defaultValue, int index,
                             CodeBlock modelBlock, boolean defineProperty) {
            ColumnAccessor wrapperLevelAccessor = combiner.wrapperLevelAccessor;
            builder.addStatement("clause.and($L.$Leq($L))", columnRepresentation,
                    !wrapperLevelAccessor.isPrimitiveTarget()? "invertProperty()." : "",
                        getFieldAccessBlock(builder, modelBlock, !(wrapperLevelAccessor instanceof ColumnAccessor.BooleanColumnAccessor), true));
        }

        @Override
        public void addNull(CodeBlock.Builder code, String columnRepresentation, int index) {
            code.addStatement("clause.and($L.eq(($T) $L))", columnRepresentation,
                    ClassNames.ICONDITIONAL, "null");
        }
    }

    public static class UpdateAutoIncrementAccessCombiner extends ColumnAccessCombiner {
        public UpdateAutoIncrementAccessCombiner(Combiner combiner) {
            super(combiner);
        }

        @Override
        public void addCode(CodeBlock.Builder builder, String columnRepresentation, CodeBlock defaultValue,
                            int index, CodeBlock modelBlock, boolean defineProperty) {
            String method = "";
            if (SQLiteHelper.containsNumberMethod(combiner.fieldTypeName.unbox())) {
                method = combiner.fieldTypeName.unbox().toString();
            }

            builder.addStatement(combiner.fieldLevelAccessor.set(CodeBlock.of("id.$LValue()", method), modelBlock, false));
        }

    }

    public static class CachingIdAccessCombiner extends ColumnAccessCombiner {
        public CachingIdAccessCombiner(Combiner combiner) {
            super(combiner);
        }

        @Override
        public void addCode(CodeBlock.Builder builder, String columnRepresentation,
                            CodeBlock defaultValue, int index, CodeBlock modelBlock, boolean defineProperty) {
            builder.addStatement("inValues[$L] = $L", index, getFieldAccessBlock(builder, modelBlock, true, true));
        }

    }

    public static class SaveModelAccessCombiner extends ColumnAccessCombiner {
        public boolean implementsModel;
        public boolean extendsBaseModel;

        public SaveModelAccessCombiner(Combiner combiner, boolean implementsModel, boolean extendsBaseModel) {
            super(combiner);
            this.implementsModel = implementsModel;
            this.extendsBaseModel = extendsBaseModel;
        }

        @Override
        public void addCode(CodeBlock.Builder builder, String columnRepresentation, CodeBlock defaultValue,
                            int index, CodeBlock modelBlock, boolean defineProperty) {
            CodeBlock access = getFieldAccessBlock(builder, modelBlock, true, true);
            if(access != null) {
                if (implementsModel) {
                    builder.addStatement(access+".save("+wrapperIfBaseModel(extendsBaseModel)+")").endControlFlow();
                } else {
                    builder.addStatement("$T.getModelAdapter($T.class).save("+access+", "+ModelUtils.wrapper+")",
                            ClassNames.FLOW_MANAGER, combiner.fieldTypeName).endControlFlow();
                }
            }
        }

    }

    public static class DeleteModelAccessCombiner extends ColumnAccessCombiner {
        boolean implementsModel;
        boolean extendsBaseModel;

        public DeleteModelAccessCombiner(Combiner combiner, boolean implementsModel, boolean extendsBaseModel) {
            super(combiner);
            this.implementsModel = implementsModel;
            this.extendsBaseModel = extendsBaseModel;
        }

        @Override
        public void addCode(CodeBlock.Builder builder, String columnRepresentation, CodeBlock defaultValue,
                            int index, CodeBlock modelBlock, boolean defineProperty) {
            CodeBlock access = getFieldAccessBlock(builder, modelBlock,true,true);
            if(access != null) {
                if (implementsModel) {
                    builder.addStatement(access + ".delete("+wrapperIfBaseModel(extendsBaseModel)+")").endControlFlow();
                } else {
                    builder.addStatement("$T.getModelAdapter($T.class).delete("+access+", "+ModelUtils.wrapper+")",
                            ClassNames.FLOW_MANAGER, combiner.fieldTypeName).endControlFlow();
                }
            }
        }

    }

    public static String wrapperIfBaseModel(boolean extendsBaseModel) {
        return extendsBaseModel? ModelUtils.wrapper : "";
    }

    public static String wrapperCommaIfBaseModel(boolean extendsBaseModel) {
        return extendsBaseModel? ", " + ModelUtils.wrapper : "";
    }
}

